{*******************************************************************************
* The contents of this file are used with permission, subject to the Mozilla   *
* Public License Version 1.1 (the "License"); you may not use this file except *
* in compliance with the License. You may obtain a copy of the License at      *
* http://www.mozilla.org/MPL/                                                  *
*                                                                              *
* Software distributed under the License is distributed on an "AS IS" basis,   *
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for *
* the specific language governing rights and limitations under the License.    *
*                                                                              *
* The Original Code is distributed as part of the "mediate" product and is     *
* Copyright (C) @home media limited. All Rights Reserved.                      *
*                                                                              *
* Email: support@athomemedia.co.uk                                             *
* Web:   http://www.athomemedia.co.uk                                          *
*******************************************************************************}

unit ThemeManager;

interface

uses Classes, VisualDescriptor, InterfaceDescriptor, LanguageDescriptor,
     AudioDescriptor, ThemeDescriptor, Descriptor, MetaValues, UIObjects,
     XMLHelper, AHMTypes;

const
  THEME_RESOURCE_PATH = 'Resources';
  BASE_DESCRIPTOR_PREFIX = 'base';
  DESCRIPTOR_NAME_SEP = '-';

{$IFDEF DESIGNER}
  // Constants used with theme designer only
  BROWSE_FILE_TITLE = 'Select file';
  COPY_FILE_CONFIRM = 'Would you like to copy this file into the current ' +
                      'theme resources directory?';
{$ENDIF}

type
  TAHMDescriptorTypes = (dtVisual, dtInterface, dtLanguage, dtAudio);
  TAHMDescriptors = set of TAHMDescriptorTypes;

  TAHMThemeModuleList = class(TStringList);
  TAHMModuleDescriptions = class(TStringList);

  TAHMThemeManager = class
  private
    FCache: Boolean;
    FNewTheme: Boolean;
    FThemeName: String;
    FThemeGroup: String;
    FThemeSettings: TAHMMetaValueList;
    FInstalledThemes: TStringList;
    FInstalledModules: TAHMThemeModuleList;
    FModuleDescriptions: TAHMModuleDescriptions;
    FDescriptors: TAHMDescriptors;
    FThemeDescriptor: TAHMThemeDescriptor;
    FBaseVisual, FVisualDescriptor: TAHMVisualDescriptor;
    FBaseInterface, FInterfaceDescriptor: TAHMInterfaceDescriptor;
    FBaseLanguage, FLanguageDescriptor: TAHMLanguageDescriptor;
    FBaseAudio, FAudioDescriptor: TAHMAudioDescriptor;
    FVisualDescriptors: TAHMDescriptorList;
    FInterfaceDescriptors: TAHMDescriptorList;
    FLanguageDescriptors: TAHMDescriptorList;
    FAudioDescriptors: TAHMDescriptorList;
    FGarbageDescriptors: TAHMDescriptorList;
  protected
    function GetInstalledThemes: TStringList;
    function GetInstalledModules: TAHMThemeModuleList;
    function GetModuleDescriptions: TAHMModuleDescriptions;
    function GetModuleLoaded(Index: Integer): Boolean;
    procedure RefreshInstalledModules;
    procedure RefreshModuleDescriptions;
    function GetThemeURLPrefix: String;
    function GetBaseDescriptorURLPrefix: String;
    function GetGroupDescriptorURLPrefix: String;
    function GetDescriptorURLPrefix(Group: String): String;
    function GetThemeResourceURL: String;
    function GetObjectList(ObjectType: TAHMUIObjectType): TAHMUIObjectList;
    function CheckDescriptorExists(const DescriptorFile: String): Boolean;
    function CheckDescriptorsAvailable(DescriptorName: String): Boolean;
    procedure SeedNewThemeContents;
    procedure UnloadDescriptor;
    procedure CreateDescriptors;
    function LoadAllDescriptors: Boolean;
  public
    constructor Create;
    destructor Destroy; override;
    property CacheDescriptors: Boolean read FCache write FCache;
    property InstalledThemes: TStringList read GetInstalledThemes;
    property InstalledModules: TAHMThemeModuleList read GetInstalledModules;
    property ModuleDescriptions: TAHMModuleDescriptions read GetModuleDescriptions;
    property ModuleLoaded[Index: Integer]: Boolean read GetModuleLoaded;
    property ThemeName: String read FThemeName;
    property ThemeGroup: String read FThemeGroup;
    property ThemeResourceURL: String read GetThemeResourceURL;
    property ThemeDescriptor: TAHMThemeDescriptor read FThemeDescriptor;
    property ThemeObjects[ObjectType: TAHMUIObjectType]: TAHMUIObjectList read GetObjectList;
    property VisualDescriptor: TAHMVisualDescriptor read FVisualDescriptor;
    property InterfaceDescriptor: TAHMInterfaceDescriptor read FInterfaceDescriptor;
    property LanguageDescriptor: TAHMLanguageDescriptor read FLanguageDescriptor;
    property AudioDescriptor: TAHMAudioDescriptor read FAudioDescriptor;
    property VisualDescriptors: TAHMDescriptorList read FVisualDescriptors;
    property InterfaceDescriptors: TAHMDescriptorList read FInterfaceDescriptors;
    property LanguageDescriptors: TAHMDescriptorList read FLanguageDescriptors;
    property AudioDescriptors: TAHMDescriptorList read FAudioDescriptors;
    function ValidateThemeName(ThemeName: String; NewTheme: Boolean): Boolean;
    function ValidateDescriptorName(GroupName: String): Boolean;
    function CheckThemeExists(ThemeName: String): String;
    procedure NewTheme(NewDescriptor: TAHMThemeDescriptor);
    function LoadTheme(ThemeName: String): Boolean;
    function LoadDescriptor(GroupName: String): Boolean;
    function AddDescriptor(GroupName: String): Boolean;
    function CopyDescriptor(SourceName, TargetName: String): Boolean;
    function DeleteDescriptor(GroupName: String): Boolean;
    function RenameDescriptor(OldName, NewName: String): Boolean;
    function SaveTheme(ThemeName: String = ''): Boolean;
    function ImportTheme(const Filename, NewName: String): Boolean;
    function ExportTheme(const Filename: String): Boolean;
    function ValidateThemeArchive(const Filename: String; var Name, Description: String): Boolean;
    function AddThemeResource(var Filename: String; ResourceType: TAHMResourceType): Boolean;
    procedure AllocateResources;
    procedure ReleaseResources;
    procedure LoadThemeSettings(XML: TAHMXMLHelper);
    procedure ApplyThemeSettings;
    procedure UpdateThemeSettings;
    procedure SaveThemeSettings(Lines: TStrings);
  end;


implementation

uses SysUtils, ApplicationManager, Screen, Languages, MetaData, MetaConsts,
     {$IFDEF DESIGNER} Dialogs, Controls, Colours, Fonts, Captions, Places,
     BaseControls, UIControls, Containers, Screens, Canvas, Fills, Shapes,
     GridItems, {$ENDIF} JclFileUtils, Utilities, CompressionHelper, AHMConsts;

constructor TAHMThemeManager.Create;
begin
  inherited Create;

  // Create stringlist to store list of installed themes & modules
  FInstalledThemes := TStringList.Create;
  FInstalledModules := TAHMThemeModuleList.Create;
  FModuleDescriptions := TAHMModuleDescriptions.Create;
end;

destructor TAHMThemeManager.Destroy;
begin
  // Release cached descriptors and other theme objects
  FInstalledThemes.Free;
  FInstalledModules.Free;
  FModuleDescriptions.Free;

  inherited Destroy;
end;

function TAHMThemeManager.GetInstalledThemes: TStringList;
var
  Found, Attr: Integer;
  SearchRec: TSearchRec;
begin
  // Update list of available themes in current themes directory
  FInstalledThemes.Clear;
  FInstalledThemes.CaseSensitive := False;

  // Set up directory search parameters
  Attr := faDirectory;
  Found := FindFirst(ApplicationManagerInstance.ThemePath + '*.*', Attr, SearchRec);
  while Found = 0 do
  begin
     // Ignore . and .. dummy directories
    if (SearchRec.Attr and faDirectory) <> 0 then
      if Copy(SearchRec.Name, 1, 1) <> '.' then
        FInstalledThemes.Add(SearchRec.Name);
    Found := FindNext(SearchRec);
  end;
  FindClose(SearchRec);

  // Return pointer to our stringlist
  Result := FInstalledThemes;
end;

function TAHMThemeManager.GetInstalledModules: TAHMThemeModuleList;
begin
  // Refresh installed modules if queried for the first time
  if FInstalledModules.Count = 0 then RefreshInstalledModules;
  Result := FInstalledModules;
end;

function TAHMThemeManager.GetModuleDescriptions: TAHMModuleDescriptions;
begin
  // Refresh module descriptions if queried for the first time
  if FModuleDescriptions.Count = 0 then RefreshModuleDescriptions;
  Result := FModuleDescriptions;
end;

function TAHMThemeManager.GetModuleLoaded(Index: Integer): Boolean;
begin
  // Determine whether specified module is currently loaded / active
  if Index = 0 then
    Result := True
  else
    Result := (UpperCase(InstalledModules[Index]) = UpperCase(FThemeGroup));
end;

procedure TAHMThemeManager.RefreshInstalledModules;
var
  Found, Attr, Sep: Integer;
  Name: String;
  SearchRec: TSearchRec;
begin
  // Update list of available modules for current themes directory
  FInstalledModules.Clear;
  FInstalledModules.Sorted := True;
  FInstalledModules.Duplicates := dupIgnore;
  FInstalledModules.CaseSensitive := False;

  // No need to scan filesystem for new themes
  if not FNewTheme then
  begin
    // Set up directory search parameters
    Attr := faAnyFile;
    Found := FindFirst(GetThemeURLPrefix + '*' + EXT_THEME, Attr, SearchRec);
    while Found = 0 do
    begin
      // Extract prefix for this XML descriptor
      Sep := Pos(DESCRIPTOR_NAME_SEP, SearchRec.Name) - 1;
      if Sep >= 0 then
      begin
        // Ignore duplicates
        Name := LowerCase(Copy(SearchRec.Name, 1, Sep));
        if Name <> BASE_DESCRIPTOR_PREFIX then
          FInstalledModules.Add(Name);
      end;
      Found := FindNext(SearchRec);
    end;
    FindClose(SearchRec);
  end;

  // Unsort the list and insert the base descriptor at the top
  FInstalledModules.Sorted := False;
  FInstalledModules.Insert(0, BASE_DESCRIPTOR_PREFIX);
end;

procedure TAHMThemeManager.RefreshModuleDescriptions;
var
  i: Integer;
  Description: String;
begin
  // Ensure module list is up to date first
  GetInstalledModules;
  FModuleDescriptions.Clear;

  for i := 0 to Pred(FInstalledModules.Count) do
  begin
    Description := FInstalledModules[i];
    if Length(Description) > 0 then
    begin
      Description[1] := UpCase(Description[1]);
      Description := Description + ' module';
    end;
    FModuleDescriptions.Add(Description);
  end;
end;

function TAHMThemeManager.GetThemeURLPrefix: String;
begin
  // Return URL prefix for current theme
  Result := ApplicationManagerInstance.ThemePath + FThemeName + PathDelim;
end;

function TAHMThemeManager.GetBaseDescriptorURLPrefix: String;
begin
  // Return URL prefix for base theme descriptors
  Result := GetDescriptorURLPrefix(BASE_DESCRIPTOR_PREFIX);
end;

function TAHMThemeManager.GetGroupDescriptorURLPrefix: String;
begin
  // Return URL prefix for additional theme descriptors
  Result := GetDescriptorURLPrefix(FThemeGroup);
end;

function TAHMThemeManager.GetDescriptorURLPrefix(Group: String): String;
begin
  // Return URL prefix for specified descriptor group
  Result := GetThemeURLPrefix + Group + DESCRIPTOR_NAME_SEP;
end;

function TAHMThemeManager.GetThemeResourceURL: String;
begin
  // Return URL prefix for theme media resources
  Result := GetThemeURLPrefix + THEME_RESOURCE_PATH + PathDelim;
end;

function TAHMThemeManager.GetObjectList(ObjectType: TAHMUIObjectType): TAHMUIObjectList;
begin
  // Return appropriate UIObject list
  case ObjectType of
    // Visual descriptor stuff
    otColours:       Result := VisualDescriptor.ColourList;
    otFills:         Result := VisualDescriptor.FillList;
    otPlasma:        Result := VisualDescriptor.PlasmaList;
    otFonts:         Result := VisualDescriptor.FontList;
    otBitmaps:       Result := VisualDescriptor.BitmapList;
    otBackgrounds:   Result := VisualDescriptor.BackgroundList;
    otForegrounds:   Result := VisualDescriptor.ForegroundList;
    otShapes:        Result := VisualDescriptor.ShapeList;
    otCaptions:      Result := VisualDescriptor.CaptionList;
    otMediaWindows:  Result := VisualDescriptor.MediaWindowList;
    otBars:          Result := VisualDescriptor.BarList;
    otButtons:       Result := VisualDescriptor.ButtonList;
    otMediaGrids:    Result := VisualDescriptor.MediaGridList;
    otGrids:         Result := VisualDescriptor.GridList;
    otGridItems:     Result := VisualDescriptor.GridItemList;
    otTextEdits:     Result := VisualDescriptor.TextEditList;
    otEffects:       Result := VisualDescriptor.EffectList;
    otPluginWindows: Result := VisualDescriptor.PluginWindowList;
    otGauges:        Result := VisualDescriptor.GaugeList;
    otDials:         Result := VisualDescriptor.DialList;
    // Function descriptor stuff
    otFunctions:     Result := ApplicationManagerInstance.FunctionDescriptor.FunctionList;
    // Interface descriptor stuff
    otExpressions:   Result := InterfaceDescriptor.ExpressionList;
    otMacros:        Result := InterfaceDescriptor.MacroList;
    otActions:       Result := InterfaceDescriptor.ActionList;
    otMetaInfo:      Result := InterfaceDescriptor.MetaList;
    otPlaces:        Result := InterfaceDescriptor.PlaceList;
    otAnimators:     Result := InterfaceDescriptor.AnimatorList;
    otLayouts:       Result := InterfaceDescriptor.LayoutList;
    otControls:      Result := InterfaceDescriptor.ControlList;
    otContainers:    Result := InterfaceDescriptor.ContainerList;
    otScreens:       Result := InterfaceDescriptor.ScreenList;
    otTimers:        Result := InterfaceDescriptor.TimerList;
    // Audio descriptor stuff
    otSounds:        Result := AudioDescriptor.SoundList;
    // Language descriptor stuff
    otLanguages:     Result := LanguageDescriptor.LanguageManager.Languages;
    else             Result := nil;
  end;
end;

function TAHMThemeManager.ValidateThemeName(ThemeName: String; NewTheme: Boolean): Boolean;
begin
  // Set default result - not valid
  Result := False;

  if ValidFileName(ThemeName) then
    if NewTheme then
      Result := not DirectoryExists(ApplicationManagerInstance.ThemePath + ThemeName)
    else
      Result := True;
end;

function TAHMThemeManager.ValidateDescriptorName(GroupName: String): Boolean;
begin
  // Set default result - not valid
  Result := False;

  if ValidFileName(GroupName) then
    if GetInstalledModules.IndexOf(GroupName) < 0 then
      Result := True;
end;

function TAHMThemeManager.CheckThemeExists(ThemeName: String): String;
begin
  // Set default result - suggested theme
  Result := ThemeName;

  // Check whether specified theme is installed - if not choose another
  with GetInstalledThemes do
    if (Count > 0) and (IndexOf(ThemeName) < 0) then
      Result := Strings[0];
end;

function TAHMThemeManager.CheckDescriptorExists(const DescriptorFile: String): Boolean;
begin
  // Check whether custom descriptor file is present
  Result := FileExists(GetGroupDescriptorURLPrefix + DescriptorFile);
end;

procedure TAHMThemeManager.NewTheme(NewDescriptor: TAHMThemeDescriptor);
begin
  // Note: this method must only be called when theme manager is not running
  // (i.e. AllocateResources has not yet been called, or ReleaseResources has
  // been called to free any resources in use.

  // Store the new theme descriptor to use when we start up
  FThemeDescriptor := NewDescriptor;
end;

function TAHMThemeManager.LoadTheme(ThemeName: String): Boolean;
begin
  // Set default result - problem loading theme
  Result := False;

  // Validate theme actually exists and set internal theme name property
  if FNewTheme then FThemeName := ThemeName
               else FThemeName := CheckThemeExists(ThemeName);

  // Load theme descriptor
  if FNewTheme or FThemeDescriptor.ParseXML(GetThemeURLPrefix) then
  begin
    // Set virtual dimensions for screen to use with remaining descriptors
    ScreenInstance.VirtualWidth := FThemeDescriptor.DesignedWidth;
    ScreenInstance.VirtualHeight := FThemeDescriptor.DesignedHeight;

    // Load visual descriptor
    if FNewTheme or FBaseVisual.ParseXML(GetBaseDescriptorURLPrefix) then
    begin
      // Load interface descriptor
      if FNewTheme or FBaseInterface.ParseXML(GetBaseDescriptorURLPrefix) then
      begin
        // Load language descriptor
        if FNewTheme or FBaseLanguage.ParseXML(GetBaseDescriptorURLPrefix) then
        begin
          // Load audio descriptor
          if FNewTheme or FBaseAudio.ParseXML(GetBaseDescriptorURLPrefix) then
          begin
            // Seed a brand new theme with a few defaults
            if FNewTheme then SeedNewThemeContents;

            // Apply theme & persistent settings to metadata manager
            FThemeSettings.Apply;
            ApplicationManagerInstance.StateManager.MetaSettings.Apply;

            // Initialise all base descriptors
            FLanguageDescriptor.Initialise;
            FVisualDescriptor.Initialise;
            FAudioDescriptor.Initialise;
            FInterfaceDescriptor.Initialise;

            // If cache enabled then load all custom descriptors too
            if FCache then Result := LoadAllDescriptors
                      else Result := True;
          end;
        end;
      end;
    end;
  end;
end;

function TAHMThemeManager.SaveTheme(ThemeName: String): Boolean;
{$IFDEF DESIGNER}
var
  i: Integer;
  OldResourceURL: String;
{$ENDIF}
begin
{$IFDEF DESIGNER}
  // If a new theme name was supplied then update theme name, but
  // keep a copy of the current resource path so we can copy resources
  OldResourceURL := GetThemeResourceURL;
  if ThemeName <> '' then FThemeName := ThemeName;

  // Ensure directory structure exists for saving theme
  ForceDirectories(GetThemeURLPrefix);

  // Clear down any previous descriptors in case they've been renamed
  FileDelete(GetThemeURLPrefix + '*' + EXT_THEME, True);

  // Update and save theme descriptor
  FThemeDescriptor.Updated := Now;
  FThemeDescriptor.Name := FThemeName;
  FThemeDescriptor.Directory := FThemeName;
  FThemeDescriptor.SaveAsXML(GetThemeURLPrefix);

  // Save cached descriptors
  with FVisualDescriptors do
    for i := 0 to Pred(Count) do
      Descriptor[i].SaveAsXML(GetDescriptorURLPrefix(Descriptor[i].Name));
  with FInterfaceDescriptors do
    for i := 0 to Pred(Count) do
      Descriptor[i].SaveAsXML(GetDescriptorURLPrefix(Descriptor[i].Name));
  with FLanguageDescriptors do
    for i := 0 to Pred(Count) do
      Descriptor[i].SaveAsXML(GetDescriptorURLPrefix(Descriptor[i].Name));
  with FAudioDescriptors do
    for i := 0 to Pred(Count) do
      Descriptor[i].SaveAsXML(GetDescriptorURLPrefix(Descriptor[i].Name));

  // If saving theme under a new name we need to copy resources folder too
  if ThemeName <> '' then
    Result := CopyDirectory(OldResourceURL, GetThemeResourceURL)
  else
    Result := True;

  // Success! We no longer treat this as a new theme
  FNewTheme := False;
{$ELSE}
  Result := False;
{$ENDIF}
end;

procedure TAHMThemeManager.SeedNewThemeContents;
{$IFDEF DESIGNER}
var
  Language: TAHMLanguage;
  TextColour: TAHMColour;
  Shape: TAHMShape;
  Caption: TAHMCaption;
  ContainerPlace: TAHMPlace;
  ControlPlace: TAHMPlace;
  CaptionControl: TAHMControl;
  Container: TAHMContainer;
  ScreenLayout: TAHMScreenLayout;
{$ENDIF}
begin
{$IFDEF DESIGNER}
  // Create a default english language
  with FBaseLanguage.LanguageManager do
  begin
    Language := Languages.AddLanguage('', 'English');
    Language.Description := 'English';
    NativeLanguageName := 'English';
    PreferredLanguageName := 'English';

    Language.AddTranslation('hello', 'hello world!');
    Language.AddTranslation(META_BOOLEANS_YES, 'Yes');
    Language.AddTranslation(META_BOOLEANS_NO, 'No');
    Language.AddTranslation('system.dateformat','d/m/yy');
    Language.AddTranslation('system.timeformat', 'h:nn');
  end;

  // Create some default visual stuff
  with FBaseVisual do
  begin
    TextColour := ColourList.AddColour(DEFAULT_OBJECT_NAME, DEFAULT_WHITE_NAME);
    TextColour.ColourDef := DEFAULT_WHITE_COLOUR;

    FillList.CreateDefault(TAHMGradedFill);
    FontList.CreateDefault(TAHMFont);

    ShapeList.CreateDefault(TAHMShape);
    Shape := ShapeList.AddShape(DEFAULT_OBJECT_NAME, DEFAULT_CIRCLE_NAME);
    Shape.Geometry := gtEllipse;
    Shape.VirtualHeight := DEFAULT_CTRL_HEIGHT div 4;
    Shape.VirtualWidth := DEFAULT_CTRL_HEIGHT div 4;

    CaptionList.CreateDefault(TAHMCaption);
    Caption := CaptionList.AddCaption(DEFAULT_OBJECT_NAME, 'HelloCaption');
    Caption.Description := 'Caption for hello world text';
    Caption.Align := taCenter;

    GridItemList.CreateDefault(TAHMGridItem);
  end;

  // Create some default interface stuff
  with FBaseInterface do
  begin
    ControlPlace := PlaceList.AddPlace('', 'HelloControlPlace');
    ControlPlace.Description := 'Place for hello world caption';
    ControlPlace.VerticalAlign := vaCenter;
    ControlPlace.HorizontalAlign := haCenter;

    ContainerPlace := PlaceList.AddPlace('', 'HelloContainerPlace');
    ContainerPlace.Description := 'Place for hello world container';
    ContainerPlace.VerticalAlign := vaParent;
    ContainerPlace.HorizontalAlign := haParent;

    CaptionControl := ControlList.AddControl('', 'HelloCaption', TAHMCaptionControl);
    CaptionControl.Style := Caption;
    CaptionControl.Placement := ControlPlace;
    CaptionControl.Text := 'hello';

    Container := ContainerList.AddContainer('', 'HelloContainer');
    Container.Placement := ContainerPlace;
    Container.Controls.Add(CaptionControl);

    ScreenLayout := ScreenList.AddScreenLayout('', 'HelloScreen');
    ScreenLayout.Contents.Add(Container);
  end;

  // Mark our new base descriptor as parsed so we don't try to load it
  FBaseVisual.Parsed := True;
  FBaseInterface.Parsed := True;
  FBaseLanguage.Parsed := True;
  FBaseAudio.Parsed := True;
{$ENDIF}
end;

procedure TAHMThemeManager.UnloadDescriptor;
begin
  // If we have custom descriptors then free them (unless cacheing)
  if not FCache then
  begin
    if (FLanguageDescriptor <> FBaseLanguage) then FLanguageDescriptors.Delete(FLanguageDescriptor);
    if (FInterfaceDescriptor <> FBaseInterface) then FInterfaceDescriptors.Delete(FInterfaceDescriptor);
    if (FVisualDescriptor <> FBaseVisual) then FVisualDescriptors.Delete(FVisualDescriptor);
    if (FAudioDescriptor <> FBaseAudio) then FAudioDescriptors.Delete(FAudioDescriptor);
  end;

  FLanguageDescriptor := nil;
  FInterfaceDescriptor := nil;
  FVisualDescriptor := nil;
  FAudioDescriptor := nil;

  // Reset global pointer to base language manager to simplify usage
  LanguageManagerInstance := FBaseLanguage.LanguageManager;
end;

function TAHMThemeManager.CheckDescriptorsAvailable(DescriptorName: String): Boolean;
var
  sCurrentDescriptor: String;
begin
  // Set default result - no descriptors available
  Result := False;

  // If all the theme descriptors are cached (in designer mode), then just
  // create a full set of descriptors. No point checking the filesystem
  // as the theme can exist entirely in memory
  if FCache then
  begin
    FDescriptors := [dtVisual, dtInterface, dtLanguage, dtAudio];
    Result := True;
    Exit;
  end;

  // Clear current set of available descriptors
  FDescriptors := [];

  // Temporarily set descriptor name property
  sCurrentDescriptor := FThemeGroup;
  FThemeGroup := DescriptorName;
  try
    // Determine which descriptors are available
    if CheckDescriptorExists(FILE_VISUAL_DESCRIPTOR) then
      Include(FDescriptors, dtVisual);
    if CheckDescriptorExists(FILE_INTERFACE_DESCRIPTOR) then
      Include(FDescriptors, dtInterface);
    if CheckDescriptorExists(FILE_LANGUAGE_DESCRIPTOR) then
      Include(FDescriptors, dtLanguage);
    if CheckDescriptorExists(FILE_AUDIO_DESCRIPTOR) then
      Include(FDescriptors, dtAudio);
  finally
    // Restore original descriptor name property
    FThemeGroup := sCurrentDescriptor;
  end;

  // If at least one descriptor found, return success
  if FDescriptors <> [] then
    Result := True;
end;

procedure TAHMThemeManager.CreateDescriptors;
begin
  // By default we'll point to our base descriptors
  FVisualDescriptor := FBaseVisual;
  FInterfaceDescriptor := FBaseInterface;
  FLanguageDescriptor := FBaseLanguage;
  FAudioDescriptor := FBaseAudio;

  // Create/copy required descriptor objects - look in cache first
  if dtVisual in FDescriptors then
  begin
    FVisualDescriptor := TAHMVisualDescriptor(FVisualDescriptors[FThemeGroup]);
    if FVisualDescriptor = nil then
      FVisualDescriptor := TAHMVisualDescriptor(FVisualDescriptors.Add(TAHMVisualDescriptor, FThemeGroup));
    FVisualDescriptor.BaseDescriptor := FBaseVisual;
  end;

  if dtInterface in FDescriptors then
  begin
    FInterfaceDescriptor := TAHMInterfaceDescriptor(FInterfaceDescriptors[FThemeGroup]);
    if FInterfaceDescriptor = nil then
      FInterfaceDescriptor := TAHMInterfaceDescriptor(FInterfaceDescriptors.Add(TAHMInterfaceDescriptor, FThemeGroup));
    FInterfaceDescriptor.BaseDescriptor := FBaseInterface;
  end;

  if dtLanguage in FDescriptors then
  begin
    FLanguageDescriptor := TAHMLanguageDescriptor(FLanguageDescriptors[FThemeGroup]);
    if FLanguageDescriptor = nil then
      FLanguageDescriptor := TAHMLanguageDescriptor(FLanguageDescriptors.Add(TAHMLanguageDescriptor, FThemeGroup));
    FLanguageDescriptor.BaseDescriptor := FBaseLanguage;
  end;

  if dtAudio in FDescriptors then
  begin
    FAudioDescriptor := TAHMAudioDescriptor(FAudioDescriptors[FThemeGroup]);
    if FAudioDescriptor = nil then
      FAudioDescriptor := TAHMAudioDescriptor(FAudioDescriptors.Add(TAHMAudioDescriptor, FThemeGroup));
    FAudioDescriptor.BaseDescriptor := FBaseAudio;
  end;

  // Custom descriptors are non-mandatory when running in cached (designer)
  // mode since we must always create a full set of descriptors regardless of
  // what files actually exist or have been persisted
  if FCache then
  begin
    FVisualDescriptor.Mandatory := False;
    FInterfaceDescriptor.Mandatory := False;
    FLanguageDescriptor.Mandatory := False;
    FAudioDescriptor.Mandatory := False;
  end;

  // Set global pointer to custom language manager to simplify usage
  LanguageManagerInstance := FLanguageDescriptor.LanguageManager;
end;

function TAHMThemeManager.LoadAllDescriptors: Boolean;
var
  i: Integer;
  Modules: TStrings;
begin
  // Set default result - assume success
  Result := True;

  // Get a list of known modules for theme
  Modules := GetInstalledModules;
  try
    // flag failure, but attempt to load all custom descriptors
    for i := 1 to Pred(Modules.Count) do
      if not LoadDescriptor(Modules[i]) then
        Result := False;
  finally
    // Point back to our base descriptors
    FVisualDescriptor := FBaseVisual;
    FInterfaceDescriptor := FBaseInterface;
    FLanguageDescriptor := FBaseLanguage;
    FAudioDescriptor := FBaseAudio;

    // Reset global pointer to base language manager
    LanguageManagerInstance := FBaseLanguage.LanguageManager;

    // Clear custom descriptor property
    FThemeGroup := '';
  end;
end;

function TAHMThemeManager.LoadDescriptor(GroupName: String): Boolean;
begin
  // Set default result - problem loading descriptor
  Result := False;

  // Check first we have at least one descriptor available
  if not CheckDescriptorsAvailable(GroupName) then Exit;

  // Set internal descriptor name property
  FThemeGroup := GroupName;

  // Unload current descriptor objects & create required new ones
  UnloadDescriptor;
  CreateDescriptors;

  // Load visual descriptor
  if (FVisualDescriptor = FBaseVisual) or
     FVisualDescriptor.ParseXML(GetGroupDescriptorURLPrefix) then
  begin
    // Load interface descriptor
    if (FInterfaceDescriptor = FBaseInterface) or
       FInterfaceDescriptor.ParseXML(GetGroupDescriptorURLPrefix) then
    begin
      // Load language descriptor
      if (FLanguageDescriptor = FBaseLanguage) or
         FLanguageDescriptor.ParseXML(GetGroupDescriptorURLPrefix) then
      begin
        // Load audio descriptor
        if (FAudioDescriptor = FBaseAudio) or
          FAudioDescriptor.ParseXML(GetGroupDescriptorURLPrefix) then
        begin
          // Initialise all custom descriptors
          if FLanguageDescriptor <> FBaseLanguage then FLanguageDescriptor.Initialise;
          if FVisualDescriptor <> FBaseVisual then FVisualDescriptor.Initialise;
          if FAudioDescriptor <> FBaseAudio then FAudioDescriptor.Initialise;
          if FInterfaceDescriptor <> FBaseInterface then FInterfaceDescriptor.Initialise;

          // Success - if no exceptions raised
          Result := True;
        end;
      end;
    end;
  end;
end;

function TAHMThemeManager.AddDescriptor(GroupName: String): Boolean;
var
  i: Integer;
  LangDesc: TAHMDescriptor;
  Language: TAHMLanguage;
begin
  // Set default result - failed to add
  Result := False;

  // Append descriptor name in list of installed modules
  i := FInstalledModules.IndexOf(GroupName);
  if i < 0 then FInstalledModules.Add(GroupName)
           else Exit;

  // Create a set of descriptors with requested name. Don't try to load them!
  FVisualDescriptors.Add(TAHMVisualDescriptor, GroupName).Parsed := True;
  FInterfaceDescriptors.Add(TAHMInterfaceDescriptor, GroupName).Parsed := True;
  FAudioDescriptors.Add(TAHMAudioDescriptor, GroupName).Parsed := True;

  // Create a default english language
  LangDesc := FLanguageDescriptors.Add(TAHMLanguageDescriptor, GroupName);
  with LangDesc as TAHMLanguageDescriptor do
  begin
    Parsed := True;
    Language := LanguageManager.Languages.AddLanguage('', 'English');
    Language.Description := 'English';
  end;

  // Clear description cache
  FModuleDescriptions.Clear;

  // Success
  Result := True;
end;

function TAHMThemeManager.CopyDescriptor(SourceName, TargetName: String): Boolean;
var
  i: Integer;
  Source, Target: TAHMDescriptor;
begin
  // Set default result - failed to copy
  Result := False;

  // Append descriptor name in list of installed modules
  i := FInstalledModules.IndexOf(TargetName);
  if i < 0 then FInstalledModules.Add(TargetName)
           else Exit;

  // Create a set of descriptors. Assign any that have matching sources
  Source := FVisualDescriptors.DescriptorByName[SourceName];
  Target := FVisualDescriptors.Add(TAHMVisualDescriptor, TargetName);
  if Assigned(Source) then Target.Assign(Source) else Target.Parsed := True;
  Target.Name := TargetName;

  Source := FInterfaceDescriptors.DescriptorByName[SourceName];
  Target := FInterfaceDescriptors.Add(TAHMInterfaceDescriptor, TargetName);
  if Assigned(Source) then Target.Assign(Source) else Target.Parsed := True;
  Target.Name := TargetName;

  Source := FLanguageDescriptors.DescriptorByName[SourceName];
  Target := FLanguageDescriptors.Add(TAHMLanguageDescriptor, TargetName);
  if Assigned(Source) then Target.Assign(Source) else Target.Parsed := True;
  Target.Name := TargetName;

  Source := FAudioDescriptors.DescriptorByName[SourceName];
  Target := FAudioDescriptors.Add(TAHMAudioDescriptor, TargetName);
  if Assigned(Source) then Target.Assign(Source) else Target.Parsed := True;
  Target.Name := TargetName;

  // Clear description cache
  FModuleDescriptions.Clear;

  // Success
  Result := True;
end;

function TAHMThemeManager.DeleteDescriptor(GroupName: String): Boolean;
var
  i: Integer;
  Descriptor: TAHMDescriptor;
begin
  // Set default result - failed to delete
  Result := False;

  // Move matching descriptors to garbage list
  Descriptor := FLanguageDescriptors.DescriptorByName[GroupName];
  if FLanguageDescriptors.Delete(Descriptor, False) and
     FGarbageDescriptors.Add(Descriptor) then Result := True;

  Descriptor := FInterfaceDescriptors.DescriptorByName[GroupName];
  if FInterfaceDescriptors.Delete(Descriptor, False) and
     FGarbageDescriptors.Add(Descriptor) then Result := True;

  Descriptor := FVisualDescriptors.DescriptorByName[GroupName];
  if FVisualDescriptors.Delete(Descriptor, False) and
     FGarbageDescriptors.Add(Descriptor) then Result := True;

  Descriptor := FAudioDescriptors.DescriptorByName[GroupName];
  if FAudioDescriptors.Delete(Descriptor, False) and
     FGarbageDescriptors.Add(Descriptor) then Result := True;

  if Result then
  begin
    // Remove descriptor name from list of installed modules
    i := FInstalledModules.IndexOf(GroupName);
    if i >= 0 then FInstalledModules.Delete(i);

    // Clear description cache
    FModuleDescriptions.Clear;
  end;

  // Refresh group name if deleting current group
  if FThemeGroup = GroupName then FThemeGroup := '';
end;

function TAHMThemeManager.RenameDescriptor(OldName, NewName: String): Boolean;
var
  i: Integer;
  Descriptor: TAHMDescriptor;
begin
  // Set default result - failed to rename
  Result := False;

  // Rename matching descriptors
  Descriptor := FLanguageDescriptors.DescriptorByName[OldName];
  if FLanguageDescriptors.Rename(Descriptor, NewName) then Result := True;

  Descriptor := FInterfaceDescriptors.DescriptorByName[OldName];
  if FInterfaceDescriptors.Rename(Descriptor, NewName) then Result := True;

  Descriptor := FVisualDescriptors.DescriptorByName[OldName];
  if FVisualDescriptors.Rename(Descriptor, NewName) then Result := True;

  Descriptor := FAudioDescriptors.DescriptorByName[OldName];
  if FAudioDescriptors.Rename(Descriptor, NewName) then Result := True;

  if Result then
  begin
    // Replace descriptor name in list of installed modules
    i := FInstalledModules.IndexOf(OldName);
    if i >= 0 then FInstalledModules[i] := NewName;

    // Clear description cache
    FModuleDescriptions.Clear;
  end;

  // Refresh group name if renaming current group
  if FThemeGroup = OldName then FThemeGroup := NewName;
end;

function TAHMThemeManager.ImportTheme(const Filename, NewName: String): Boolean;
var
  DestFolder: String;
begin
  // Generate target theme path - may or may not already exist
  DestFolder := ApplicationManagerInstance.ThemePath + NewName + PathDelim;

  // Ensure directory structure exists for importing theme
  ForceDirectories(DestFolder);

  // Clear down any previous descriptors in case we're overwriting
  FileDelete(DestFolder + '*' + EXT_THEME, True);

  // Now extract the archive contents into the target folder
  Result := TAHMCompressionHelper.ExtractAll(FileName, DestFolder, True);
end;

function TAHMThemeManager.ExportTheme(const Filename: String): Boolean;
begin
  // Create an archive of entire theme directory
  Result := TAHMCompressionHelper.CompressAll(Filename, GetThemeURLPrefix, True);
end;

function TAHMThemeManager.ValidateThemeArchive(const Filename: String; var Name, Description: String): Boolean;
var
  Temp: String;
  ThemeDesc: TAHMThemeDescriptor;
begin
  // Set default result - invalid theme archive
  Result := False;

  // First extract ThemeDescriptor.xml from the archive to temp dir
  Temp := ApplicationTempPath;
  if TAHMCompressionHelper.ExtractFile(Filename, Temp, FILE_THEME_DESCRIPTOR, True) then
  begin
    // Now try to parse the theme properties
    ThemeDesc := TAHMThemeDescriptor.Create;
    try
      if ThemeDesc.ParseXML(Temp) then
      begin
        // Success, parse a few theme properties. Descriptor may not exist,
        // so default theme name to archive name if it's missing
        if ThemeDesc.Name = '' then
          Name := ExtractFileCaption(ExtractFileName(Filename))
        else
          Name := ThemeDesc.Name;
        Description := ThemeDesc.Description;
        Result := True;
      end;
    finally
      ThemeDesc.Free;
    end;
  end;
end;

function TAHMThemeManager.AddThemeResource(var Filename: String; ResourceType: TAHMResourceType): Boolean;
{$IFDEF DESIGNER}
var
  Work, Filter: String;
{$ENDIF}
begin
{$IFDEF DESIGNER}
  // Browse files in theme resource directory
  Work := Trim(Filename);
  case ResourceType of
    rtImage: Filter := FILTER_IMAGES;
    rtSound: Filter := FILTER_SOUNDS;
    else Filter := FILTER_ALLFILES;
  end;
  Result := ChooseFile(BROWSE_FILE_TITLE, Filter, '', ThemeResourceURL, Work, False);

  if Result then
  begin
    Work := Trim(Work);

    // Check whether filename points to the theme resource directory
    if SameText(Copy(Work, 1, Length(ThemeResourceURL)), ThemeResourceURL) then
    begin
      // Remove theme resource prefix from filename
      Filename := Copy(Work, Length(ThemeResourceURL) + 1, Length(Work));
    end
    else
    begin
      // Prompt to copy file from another location
      if MessageDlg(COPY_FILE_CONFIRM, mtConfirmation, [mbYes, mbNo], 0) = mrYes then
      begin
        Filename := ExtractFileName(Work);
        FileCopy(Work, ThemeResourceURL + Filename);
      end
      else
        Result := False;
    end;
  end;
{$ELSE}
  Result := False;
{$ENDIF}
end;

procedure TAHMThemeManager.AllocateResources;
begin
  // Create meta value list to hold theme settings
  FThemeSettings := TAHMMetaValueList.Create;

  // Create longterm descriptor list objects
  FVisualDescriptors := TAHMDescriptorList.Create;
  FInterfaceDescriptors := TAHMDescriptorList.Create;
  FLanguageDescriptors := TAHMDescriptorList.Create;
  FAudioDescriptors := TAHMDescriptorList.Create;
  FGarbageDescriptors := TAHMDescriptorList.Create;

  // Determine whether we are creating a new theme or not
  FNewTheme := (FThemeDescriptor <> nil);
  if not FNewTheme then FThemeDescriptor := TAHMThemeDescriptor.Create;

  // Create longterm base descriptor objects & add to cache
  FBaseVisual := TAHMVisualDescriptor(FVisualDescriptors.Add(TAHMVisualDescriptor, BASE_DESCRIPTOR_PREFIX));
  FBaseInterface := TAHMInterfaceDescriptor(FInterfaceDescriptors.Add(TAHMInterfaceDescriptor, BASE_DESCRIPTOR_PREFIX));
  FBaseLanguage := TAHMLanguageDescriptor(FLanguageDescriptors.Add(TAHMLanguageDescriptor, BASE_DESCRIPTOR_PREFIX));
  FBaseAudio := TAHMAudioDescriptor(FAudioDescriptors.Add(TAHMAudioDescriptor, BASE_DESCRIPTOR_PREFIX));

  // Point custom descriptors at base descriptor objects
  FVisualDescriptor := FBaseVisual;
  FLanguageDescriptor := FBaseLanguage;
  FInterfaceDescriptor := FBaseInterface;
  FAudioDescriptor := FBaseAudio;

  // Set global pointer to base language manager to simplify usage
  LanguageManagerInstance := FBaseLanguage.LanguageManager;
end;

procedure TAHMThemeManager.ReleaseResources;
begin
  // Release cached descriptors and other theme objects
  FreeAndNil(FLanguageDescriptors);
  FreeAndNil(FInterfaceDescriptors);
  FreeAndNil(FVisualDescriptors);
  FreeAndNil(FAudioDescriptors);
  FreeAndNil(FGarbageDescriptors);
  FreeAndNil(FThemeDescriptor);
  FreeAndNil(FThemeSettings);

  // Clear down cached theme details
  FInstalledModules.Clear;
  FModuleDescriptions.Clear;
end;

procedure TAHMThemeManager.LoadThemeSettings(XML: TAHMXMLHelper);
begin
  // Clear current settings then use standard metadata parsing
  FThemeSettings.ClearObjects;
  FThemeSettings.ParseFromXML(XML);
end;

procedure TAHMThemeManager.ApplyThemeSettings;
begin
  // Apply theme specific settings to live metadata values
  FThemeSettings.Apply;
end;

procedure TAHMThemeManager.UpdateThemeSettings;
var
  Prefix: String;
  Values: TAHMMetaData;
begin
  // Get current values in 'settings.theme' namespace
  Prefix := META_THEME_SETTINGS;
  Values := ApplicationManagerInstance.MetaDataManager.GetMetaData(Prefix);

  // Replace current values within our metadata list
  FThemeSettings.ClearObjects;
  FThemeSettings.ReadValues(Prefix, Values);
end;

procedure TAHMThemeManager.SaveThemeSettings(Lines: TStrings);
var
  i: Integer;
begin
  // Save theme specific metadata settings
  for i := 0 to Pred(FThemeSettings.Count) do
    TAHMUIObject(FThemeSettings.Objects[i]).SaveAsXML(Lines);
end;


end.


